package NetWork;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;


    // 指定一个构造方法
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("启动服务器");
        // 此处使用 CachedThreadPool , 使用 FixedThreadPool 不太合适 (线程数不太应该是有固定的...) (不适用固定的线程池数)
        ExecutorService threadPool = Executors.newCachedThreadPool(); // 创建一个线程
        while(true){
            // 使用这个 clientSocket 和具体的客户端进行交流
           Socket clientSocket = serverSocket.accept(); // accept 这个方法的效果是 接收连接, 前提是 要有客户端建立连接
            // 客户端在构造 Socket 对象的时候, 就会指定服务器端口 IP 和 端口
            // 如果没有客户端来连接, 此时 accept 就会阻塞

            // 这个代码中, 用到一个 clientSocket, 此时任意一个客户端连上来,都会返回 / 创建一个 Socket 对象
            // (Socket 就是一个 文件 , 所以 每次创建一个 clientSocket 对象 就要占用一个文件描述符表的位置)
            // 因此在使用完毕的时候就要进行一个释放
            // 我们前面的 socket 都没有释放, 一方面这些 socket 声明周期更长, 另一方面这些 socket 也不多. 固定数量
            // 但此处的 clientSocket 数量多, 每个客户端都有一个, 生命周期也要更短

            // 接下来进行交互
            //processConnection(clientSocket);
            // 接下来更新一个操作

            // 使用多线程来处理
            /*Thread thread = new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();*/
            // 但是还会存在一个问题, 如果咱们的服务器,客户端特别多
            // 很多客户端频繁来建立连接
            // 就需要频繁创建 和 销毁线程了
            // 这时候就需要有线程池来进一步优化代码

            // 接下来使用线程池
            threadPool.submit(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            // 虽然是使用了线程池了, 但是还不够
            // 如果 客户端非常多, 而且客户端连接都迟迟不断开, 就会导致咱们的机器上有很多线程 !!!
            //  C10K 和 C10M 问题
            // (C10K 是 单机处理 1w 客户端  C10M 就是 处理 1Kw个客户端)
            // 针对上述多线程版本,最大的问题是机器承担不了这么大的线程开销
            // 是否有一个办法让一个线程处理多个客户端呢?
            // 这要靠操作系统底层的知识(IO多路复用/ IO 多路转接 )
            // (IO多路复用 就是充分利用等待时间 做别的事情)
            // 给这个线程安排多个集合, 这个集合就放了一堆连接
            // 这个线程就负责监听这个集合. 哪个连接有数据来了
            // 线程就处理哪个连接...
            // 虽然连接很多,但是这些连接的请求并非严格意义的,同时总还有先有后的...
            // 在操作系统里 提供了一些原生的 API, select poll epoll
            // 在Java中 提供了一组 NIO 这样 类 , 就封装了上述多路复用的API


        }
        // TCP socket 对象里面其实涉及到两种 socket 对象 !!
        // ServerSocket 就相当于外场销售小哥哥
        // ClientSocket 就是相当于一个 内场小姐姐
        // 这两个相互配合
    }

    // 使用这个方法来处理一个连接
    // 一个连接对应到一个客户端
    // 但是这里面可能涉及到多个交互
    // (这个是核心逻辑)
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s %d] 客户端上线!\n", clientSocket.getInetAddress().toString(),clientSocket.getPort());
        // 基于 上述 socket 对象和客户端进行通信
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            // 由于要处理多个请求和响应, 也是 使用循环来进行
            while(true){
                // 1. 读取请求
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){
                    //如果没有下一个数据, 就说明读完了. (客户端关闭了连接)
                    System.out.printf("[%s %d 客户端下线\n" ,clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                // 如果有数据的话就进行读取
                // (此处 使用 的next 是一直读取到换行符/空格/其他空白符结束, 但是最终返回结果里不包含上述 空白符)
                String request = scanner.next();
                // 2. 根据请求构造响应
                String response = process(request);
                // 3. 返回响应结果
               //  outputStream 没有 write String 这样的功能, 可以把 String 里的字节数组拿出来,进行写入;
                // 也可以用 字符流 来转换一下
                PrintWriter printWriter = new PrintWriter(outputStream);
                // 此处使用 println 来写入, 让结果中带有一个 \n 换行, 方便对端来接收解析
                printWriter.println(response);
                // flush 用来刷新缓冲区, 保证当前写入的数据,确实是发送出去了
                printWriter.flush();

                System.out.printf("[%s %d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                        request,response);
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            clientSocket.close(); // 更合适的做法是把 close 返到 finally 里面 保证一定能够执行到
        }
    }

    private String process(String request) {
        return request;
    }

    // 但是这里的代码 一次只能处理一个客户端
    // 所以要写完客户端才能知道
    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
        tcpEchoServer.start();
    }


    //  当前已经写完了代码 . 但是还有一个问题存在
    // 我们 再返回 TCP 的 服务器端 和 客户端 来讨论一下
    // 都是 使用 printWriter.println(response); 来发送数据的
    // 此处的 println 会在发送数据的后面 自动加上一个 \n
    // 那么 如果不用println, 而是用 print 这个代码是否能够正确运行呢?
    // 答案是: 不能
    // (没有 \n 肯定是不行的)
    // TCP 协议 是一个面向字节流的协议, 字节流的 特性, 一次读多少个字节都可以, 那么最大的问题就是, 接收方是如何知道一次一共要读多少个字节呢
    // 这就需要咱们在传输中进行明确的约定
    // 此处代码中, 隐式约定了使用 \n 来作为当前代码的请求/响应分割约定
    // scanner.next(); 需要有一个换行符
    // 客户端 printWriter.println(request); 这里就需要给一个换行符
    // 而在 客户端的 scanner.next() 中的 next 需要输入的内容,带有一个换行符
    // 但是返回的 结果 不包含换行符
    //
}
